package model;

import enums.OperandsEnum;
import enums.TextBundleEnum;
import enums.URLEnum;
import misc.BundlesProvider;
import model.dataObjects.OperandData;
import model.dataObjects.ResultData;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
/*

    This file is part of Logica-Logike.

    Logica-Logike is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation version 3 of the License.

    Logica-Logike is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Logica-Logike.  If not, see <http://www.gnu.org/licenses/>.

 */
/**
 * Modela una tabla de verdad;
 * @author Jose Martin Rozanec;
 */
public class TruthTable implements Serializable {

    private Stack<OperandData> operandsStack;
    private Stack<ResultData> resultsStack;
    private Map<Character, Integer> variablesMap;
    private char[] expression;
    private String expressionString;

    private boolean[][] table;

    /**
     * Constructor;
     *
     * @param expression;
     */
    public TruthTable(String expression) {
        operandsStack = new Stack<OperandData>();
        resultsStack = new Stack<ResultData>();
        variablesMap = new HashMap<Character, Integer>();
        expressionString = expression;
        this.expression = expression.toCharArray();
    }

    /**
     * Devuelve el resultado de la tabla de verdad propuesta;
     *
     * @return String [][];
     */
    public String[][] getResult() {
        boolean[][] table = getTraspuesta(parseExpression());
        String[][] result = new String[table.length][table[0].length];
        for (int j = 0; j < table.length; j++) {
            for (int i = 0; i < table[j].length; i++) {
                if (table[j][i]) {
                    result[j][i] = "V";
                } else {
                    result[j][i] = "F";
                }
            }
        }
        return result;
    }

    /**
     * Devuelve la matriz traspuesta de la matriz pasada;
     *
     * @param matrix - boolean[][];
     * @return boolean[][];
     */
    private boolean[][] getTraspuesta(boolean[][] matrix) {
        boolean[][] traspuesta = new boolean[matrix[0].length][matrix.length];
        for (int j = 0; j < matrix.length; j++) {
            for (int i = 0; i < matrix[0].length; i++) {
                traspuesta[i][j] = matrix[j][i];
            }
        }
        return traspuesta;
    }

    /**
     * Realiza el analisis de las variables, para determinar los valores que debe tomar la variable en cada fila;
     */
    private void variablesAnalisis() {
        int length = expression.length;
        char current;
        int variableNumber = 1;
        for (int j = 0; j < length; j++) {
            current = expression[j];
            if (current != '0' && current != '1' && current != '2' && current != '3' && current != '4' && current != '5' && current != '6' &&
                    current != '7' && current != '(' && current != ')') {
                if (!variablesMap.containsKey(current)) {
                    variablesMap.put(current, variableNumber);
                    variableNumber++;
                }
            }
        }
    }

    /**
     * Parsea la expresion dada en el constructor y devuelve un arreglo boolean[][]
     * con los resultados de la tabla de verdad;
     *
     * @return boolean[][];
     */
    private boolean[][] parseExpression() {
        variablesAnalisis();
        String withoutParenthesis = expressionString.replace("(", "");
        withoutParenthesis = withoutParenthesis.replace(")", "");
        //table[columnas][filas]
        table = new boolean[withoutParenthesis.length()][getTableExtension()];
        int length = expression.length;
        char current;
        int priority = 0;
        boolean negated = false;
        boolean negatesParenthesis = false;
        int negationPosition = -1;
        int tablePosition = -1;//de forma que al sumar 1 para la primera variable, se busca la posicion cero.
        OperandData operandData;
        int secondColumn;
        int firstColumn;
        int resultColumn;
        int operand;
        boolean firstIsParenthesis = false;

        for (int j = 0; j < length; j++) {
            current = expression[j];
            if (current == '(') {
                priority++;
                if (negated) {
                    negatesParenthesis = true;
                } else {
                    if (tablePosition == -1) {
                        firstIsParenthesis = true;
                    }//todo see!
                }
            } else {
                if (current == ')') {
                    if (isSimpleNegation(j)) {
                        priority--;
                    } else {
                        if (tablePosition == 1 && firstIsParenthesis) {//para soportar (-p)
                            negate(negationPosition, 1);
                            resultColumn = negationPosition;
                        } else {
                            secondColumn = resultsStack.pop().getPosition();
                            firstColumn = resultsStack.pop().getPosition();
                            operandData = operandsStack.pop();
                            resultColumn = operandData.getPosition();
                            doOperation(operandData.getOperandsEnum(), firstColumn, resultColumn, secondColumn);
                            if (negatesParenthesis) {
                                negated = false;
                                negatesParenthesis = false;
                                negate(negationPosition, resultColumn);
                                resultColumn = negationPosition;
                            }
                        }
                        priority--;
                        resultsStack.push(new ResultData(resultColumn, priority));
                    }
                    //guardar el resultado con la prioridad reducida ya que se necesita en un nivel inferior
                    //ATENCION: Al no usar priority queue, la prioridad es redundante;
                } else {
                    tablePosition++;
                    if (current != '1' && current != '2' && current != '3' && current != '4' && current != '5' && current != '6' &&
                            current != '7') {//no incluye negacion=0
                        if (current == '0') {
                            negated = true;
                            negationPosition = tablePosition;
                        } else {
                            fillColumn(tablePosition, current);
                            resultColumn = tablePosition;
                            if (negated && !negatesParenthesis) {
                                negate(negationPosition, tablePosition);
                                negated = false;
                                resultColumn = negationPosition;
                            }
                            resultsStack.push(new ResultData(resultColumn, priority));
                        }
                    } else {//if current is an operand
                        operand = Integer.parseInt("" + current);
                        operandsStack.push(new OperandData(OperandsEnum.getOperand(operand), tablePosition, priority));
                    }
                }
            }
        }
        while (!operandsStack.isEmpty()) {
            secondColumn = resultsStack.pop().getPosition();
            firstColumn = resultsStack.pop().getPosition();
            operandData = operandsStack.pop();
            resultColumn = operandData.getPosition();
            doOperation(operandData.getOperandsEnum(), firstColumn, resultColumn, secondColumn);
            resultsStack.push(new ResultData(resultColumn, priority));
        }
        return table;
    }

    /**
     * Devuelve la posicion de la columna en la que se ha colocado el resultado de la tabla de verdad;
     *
     * @return int;
     */
    public int getResultColumn() {
        return resultsStack.peek().getPosition();
    }

    /**
     * Realiza la operacion correspondiente;
     *
     * @param operandsEnum - operando;
     * @param columnFirst  - valores;
     * @param columnResult - columna resultado;
     * @param columnSecond - valores;
     */
    private void doOperation(OperandsEnum operandsEnum, int columnFirst, int columnResult, int columnSecond) {
        switch (operandsEnum) {
            case CONJUNCION:
                conjuncion(columnFirst, columnResult, columnSecond);
                break;
            case INCOMPATIBILIDAD:
                incompatibilidad(columnFirst, columnResult, columnSecond);
                break;
            case DISYUNCION_INCLUSIVA:
                disyuncionInclusiva(columnFirst, columnResult, columnSecond);
                break;
            case NEGACION_CONJUNTA:
                negacionConjunta(columnFirst, columnResult, columnSecond);
                break;
            case DISYUNCION_EXCLUSIVA:
                disyuncionExclusiva(columnFirst, columnResult, columnSecond);
                break;
            case BICONDICIONAL:
                bicondicional(columnFirst, columnResult, columnSecond);
                break;
            case CONDICIONAL:
                condicional(columnFirst, columnResult, columnSecond);
                break;
        }
    }

    /**
     * Indica la cantidad de filas que tendra la tabla;
     *
     * @return int;
     */
    private int getTableExtension() {
        variablesAnalisis();
        return (int) Math.pow(2, variablesMap.size());
    }

    //tableLength=2^n where n=variables
    //table[columns][rows]
    /**
     * Rellena la columna con los valores que corresponden a la variable pasada;
     *
     * @param column;
     * @param variable;
     */
    private void fillColumn(int column, char variable) {
        int tableLength = getTableExtension();
        int variableNumber = variablesMap.get(variable);
        int cambio = (int) Math.pow(2, variableNumber) / 2;
        boolean valor = true;
        int row = 0;
        for (int i = 0; i < tableLength / cambio; i++) {
            for (int j = 0; j < cambio; j++) {
                table[column][row] = valor;
                row++;
            }
            valor = !valor;
        }
    }

    /**
     * Identifica si la negacion es una negacion sumple (a una sola variable) o si se niega una expresion;
     *
     * @param closedParenthesisPosition;
     * @return boolean;
     */
    private boolean isSimpleNegation(int closedParenthesisPosition) {
        return expression[closedParenthesisPosition - 2] == '0' &&
                expression[closedParenthesisPosition - 3] == '(';
    }

    /**
     * This method performs negate operation;
     *
     * @param resultColumn    - column where negation is stored;
     * @param referenceColumn - column that provides reference for negation;
     */
    private void negate(int resultColumn, int referenceColumn) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            table[resultColumn][j] = !table[referenceColumn][j];
        }
    }

    /**
     * Realiza la operacion de conjuncion;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void conjuncion(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if (table[columnFirst][j] && table[columnSecond][j]) {//si las dos son verdaderas
                table[columnResult][j] = true;
            } else {
                table[columnResult][j] = false;
            }
        }
    }

    /**
     * Realiza la operacion de incompatibilidad;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void incompatibilidad(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if (table[columnFirst][j] && table[columnSecond][j]) {//si las dos son verdaderas
                table[columnResult][j] = false;
            } else {
                table[columnResult][j] = true;
            }
        }
    }

    /**
     * Realiza la operacion de disyuncion inclusiva;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void disyuncionInclusiva(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if ((!table[columnFirst][j]) && (!table[columnSecond][j])) {//si las dos son falsas
                table[columnResult][j] = false;
            } else {
                table[columnResult][j] = true;
            }
        }
    }

    /**
     * Realiza la operacion de negacion conjunta;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void negacionConjunta(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if ((!table[columnFirst][j]) && (!table[columnSecond][j])) {//si las dos son falsas
                table[columnResult][j] = true;
            } else {
                table[columnResult][j] = false;
            }
        }
    }

    /**
     * Realiza la operacion de disyuncion exclusiva;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void disyuncionExclusiva(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if (((table[columnFirst][j]) && (table[columnSecond][j])) ||
                    ((!table[columnFirst][j]) && (!table[columnSecond][j]))) {//si las dos son falsas o las dos son verdaderas
                table[columnResult][j] = false;
            } else {
                table[columnResult][j] = true;
            }
        }
    }

    /**
     * Realiza la operacion bicondicional;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void bicondicional(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if (((table[columnFirst][j]) && (table[columnSecond][j])) ||
                    ((!table[columnFirst][j]) && (!table[columnSecond][j]))) {//si las dos son falsas o las dos son verdaderas
                table[columnResult][j] = true;
            } else {
                table[columnResult][j] = false;
            }
        }
    }

    /**
     * Realiza la operacion condicional;
     *
     * @param columnFirst;
     * @param columnResult;
     * @param columnSecond;
     */
    private void condicional(int columnFirst, int columnResult, int columnSecond) {
        int tableLength = getTableExtension();
        for (int j = 0; j < tableLength; j++) {
            if (table[columnSecond][j]) {//si las dos son falsas o las dos son verdaderas
                table[columnResult][j] = true;
            } else {
                if (!table[columnFirst][j]) {
                    table[columnResult][j] = true;
                } else {
                    table[columnResult][j] = false;
                }
            }
        }
    }

    public static void main(String[] args) {
        int cambio = (int) Math.pow(2, 1) / 2;
        boolean valor = true;
        for (int i = 0; i < 8 / cambio; i++) {
            for (int j = 0; j < cambio; j++) {
                System.out.println("" + valor);
            }
            valor = !valor;
        }
    }

    /**
     * Identifica si un caracter corresponde a un numero;
     *
     * @param c;
     * @return boolean;
     */
    private boolean isNumber(char c) {
        switch (c) {
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return true;
            default:
                return false;
        }
    }

    /**
     * Devuelve una representacion xml de la variable;
     *
     * @param character;
     * @return String;
     */
    private String getVariableFormat(char character) {
        switch (character) {
            case '(':
                return "<parenthesis>";
            case ')':
                return "</parenthesis>";
            default:
                return "<variable>" + character + "</variable>";
        }
    }

    /**
     * Representa la tabla de verdad en formato XML;
     *
     * @return String;
     */
    public String toXML() {
        StringBuffer xmlExpression = new StringBuffer();
        StringBuffer buffer = new StringBuffer();
        char[] charArray = expressionString.toCharArray();
        xmlExpression.append("<truth-table>");
        for (int j = 0; j < charArray.length; j++) {
            if (isNumber(charArray[j])) {
                xmlExpression.append("<operand>" + OperandsEnum.getOperand(Integer.parseInt("" + charArray[j])) + "</operand>");
            } else {
                xmlExpression.append(getVariableFormat(charArray[j]));
            }
        }
        xmlExpression.append("</truth-table>");
        return xmlExpression.toString();
    }

    /**
     * Permite obtener el String que representa la tabla de verdad;
     *
     * @return String;
     */
    public String getExpression() {
        return expressionString;
    }

    /**
     * Permite unir la presente instancia con otra tabla de verdad, para formar una tercera;
     *
     * @param operator;
     * @param newTable;
     * @param newTableBefore - indica si la newTable se halla por delante de la
     *                       presente instancia al unirlas con el operador;
     * @return TruthTable
     */
    public TruthTable joinTable(OperandsEnum operator, TruthTable newTable, boolean newTableBefore) {
        if (newTableBefore) {
            return new TruthTable(joinExpressions(newTable.getExpression(), getExpression(), operator));
        } else {
            return new TruthTable(joinExpressions(getExpression(), newTable.getExpression(), operator));
        }
    }

    /**
     * Permite unir dos String-expresiones de tablas de verdad;
     *
     * @param firstExpression;
     * @param secondExpression;
     * @param operator;
     * @return String;
     */
    private String joinExpressions(String firstExpression, String secondExpression, OperandsEnum operator) {
        return analizeParenthesis(firstExpression) + OperandsEnum.getOperandsNumber(operator) + analizeParenthesis(secondExpression);
    }

    /**
     * Realiza el analisis de los parentesis en la expresion String de la tabla de verdad;
     */
    private String analizeParenthesis(String expression) {
        boolean addParenthesis = false;
        char[] expressionArray = expression.toCharArray();
        int s = 0;
        for (int j = 0; j < expressionArray.length; j++) {
            if (expressionArray[j] == '(') {
                s++;
            } else {
                if (expressionArray[j] == ')') {
                    s--;
                }
            }
            if (s == 0) {
                addParenthesis = true;
            }
        }
        if (addParenthesis) {
            return "(" + expression + ")";
        }
        return expression;
    }

    /**
     * Parsea el XML y crea el objeto TruthTable que representa;
     *
     * @param xml;
     * @return TruthTable;
     */
    public static TruthTable fromXML(String xml) {
        String expression = xml.replace(closeTag(), "");
        expression = expression.replace(openTag(), "");
        expression = expression.replace("<operand>", "");
        expression = expression.replace("</operand>", "");
        expression = expression.replace("<variable>", "");
        expression = expression.replace("</variable>", "");
        expression = expression.replace("<parenthesis>", "(");
        expression = expression.replace("</parenthesis>", ")");
        OperandsEnum[] operands = OperandsEnum.values();
        for (int j = 0; j < operands.length; j++) {
            expression = expression.replace(operands[j].toString(), "" + OperandsEnum.getOperandsNumber(operands[j]));
        }
        return new TruthTable(expression);
    }

    /**
     * Devuelve el tag de apertura de la representacion XML de la tabla de verdad;
     *
     * @return String;
     */
    public static String openTag() {
        return "<truth-table>";
    }

    /**
     * Devuelve el tag de clausura de la representacion XML de la tabla de verdad;
     *
     * @return String;
     */
    public static String closeTag() {
        return "</truth-table>";
    }

    public String toString() {
        return replaceRepresentation();
    }

    private String replaceRepresentation() {
        String expression = getExpression();
        expression = expression.replace("0", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.NEGACION.toString()) + " ");
        expression = expression.replace("1", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.CONDICIONAL.toString()) + " ");
        expression = expression.replace("2", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.BICONDICIONAL.toString()) + " ");
        expression = expression.replace("3", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.DISYUNCION_EXCLUSIVA.toString()) + " ");
        expression = expression.replace("4", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.NEGACION_CONJUNTA.toString()) + " ");
        expression = expression.replace("5", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.DISYUNCION_INCLUSIVA.toString()) + " ");
        expression = expression.replace("6", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.INCOMPATIBILIDAD.toString()) + " ");
        expression = expression.replace("7", " " + BundlesProvider.getBundle(URLEnum.TEXT_BUNDLE).getString(
                TextBundleEnum.CONJUNCION.toString()) + " ");
        return expression;
    }

    public String[][] getArrayToComplete() {
        String withoutParenthesis = expressionString.replace("(", "");
        withoutParenthesis = withoutParenthesis.replace(")", "");
        char[] array = withoutParenthesis.toCharArray();
        String[][] result = getResult();
        System.out.println(""+result[0].length);
        System.out.println(""+array.length);
        for (int j = 0; j < array.length; j++) {
            for (int i = 0; i < result.length; i++) {
                if (isNumber(array[j])) {
                    result[i][j] = " ";
                }
            }
        }
        return result;
    }
}
